package org.xiph.vorbis
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SampleDataEvent;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.net.URLRequest;
	import flash.net.URLStream;
	import flash.utils.ByteArray;
	import flash.utils.getTimer;

	/**
	 * @author Eugene Zatepyakin
	 * @author Andre Michelle
	 */
	public final class VorbisSound extends EventDispatcher
	{
		/**
		 * The exact number of samples to be sent to the <code>SampleDataEvent.SAMPLE_DATA</code>.
		 */
		private const PLAYBACK_BUFFER_SIZE:int = 3072;

		/**
		 * The default buffer time in milliseconds
		 */
		private const DEFAULT_BUFFER_TIME: Number = 1000.0;

		//                                                  //
		//--- The values from now on must not be changed ---//
		//                                                  //
		private const SAMPLING_RATE:Number = 44100.0;
		private const SAMPLING_RATE_INV:Number = 1.0 / SAMPLING_RATE;

		private const HEADER_BYTES:int = 8192;

		private const alchemy: VorbisAlchemyDecoder = new VorbisAlchemyDecoder();
		private const alchemyRAM: ByteArray = VorbisAlchemyDecoder.ram;

		private const eventInit: Event = new Event( Event.INIT );
		private const eventSoundComplete: Event = new Event( Event.SOUND_COMPLETE );

		private const stream:URLStream = new URLStream();
		private const file:ByteArray = new ByteArray();
		private const sound:Sound = new Sound();

		private var _info: VorbisInfo;

		private var _bytesLoaded:int;
		private var _bytesTotal:int;
		private var _loaded:Boolean;
		private var _lastFileUpdate: int;

		private var _decoded:int;
		private var _initDecoder:Boolean;
		private var _outIndex:int = -1;

		private var _loop: Boolean;
		private var _bufferTime:Number = DEFAULT_BUFFER_TIME;

		private var _isPlaying:Boolean;
		private var _soundChannel: SoundChannel;

		private var _samplePosition:Number;
		private var _sampleDuration:Number;

		/**
		 * Creates a new Vorbis object.
		 *
		 * @param request A URL that points to an external Vorbis file.
		 * @param duration If avaiable, the duration of the Vorbis in seconds
		 * @param bufferTime The number of milliseconds to preload a streaming sound into a buffer before the sound starts to stream.
		 */
		public function VorbisSound( request:URLRequest = null, duration: Number = 0.0, bufferTime: Number = 1000.0 )
		{
			sound.addEventListener( SampleDataEvent.SAMPLE_DATA, sampleData );

			_soundChannel = sound.play();

			if( null != request )
			{
				load( request, duration, bufferTime );
			}
		}

		/**
		 * Initiates loading of an external Vorbis file from the specified URL.
		 *
		 * @param request A URL that points to an external Vorbis file.
		 * @param duration If avaiable, the duration of the Vorbis in seconds
		 * @param bufferTime The number of milliseconds to preload a streaming sound into a buffer before the sound starts to stream.
		 */
		public function load( request:URLRequest, duration: Number = 0.0, bufferTime: Number = 1000.0 ):void
		{
			if( null == request )
			{
				throw new Error( 'Given reference may not be null.' );
			}

			try{ stream.close(); } catch( e: Error ){}
			file.clear();

			if( null != _info )
			{
				alchemy.vorbisFree();
			}

			unregisterStreamEvents();

			_initDecoder = false;
			_decoded = 0;
			_bytesTotal = 0;
			_bytesLoaded = 0;
			_samplePosition = 0.0;
			_lastFileUpdate = 0;
			_loaded = false;
			_info = null;

			if( 0.0 == duration )
				_sampleDuration = 0.0;
			else
				_sampleDuration = duration * SAMPLING_RATE;

			_bufferTime = bufferTime;

			registerStreamEvents();
			stream.load( request );
		}

		public function loadBytes( bytes: ByteArray ): void
		{
			try{ stream.close(); } catch( e: Error ){}
			file.clear();

			if( null != _info )
			{
				alchemy.vorbisFree();
			}

			unregisterStreamEvents();

			_initDecoder = false;

			_bytesTotal =
			_bytesLoaded = bytes.length;
			_info = alchemy.vorbisInit( bytes, _bytesTotal );
			_initDecoder = true;
			_decoded = 0;
			_samplePosition = 0.0;
			_sampleDuration = alchemy.vorbisSampleDuration() * SAMPLING_RATE;
			_lastFileUpdate = 0;
			_loaded = true;

			dispatchEvent( eventInit );
		}

		/**
		 * Playback the sound.
		 */
		public function play():void
		{
			_isPlaying = true;
		}

		/**
		 * Stop playback.
		 */
		public function stop():void
		{
			_isPlaying = false;
		}

		/**
		 * Closes the stream, causing any download of data to cease
		 */
		public function close():void
		{
			unregisterStreamEvents();

			try
			{
				stream.close();
			}
			catch( e:Error )
			{
			}

			if( null != _info )
			{
				alchemy.vorbisFree();
			}

			_info = null;

			_isPlaying = false;
		}

		/**
		 * Returns SoundChannel associated with playing sound
		 */
		public function get soundChannel():SoundChannel
		{
			return _soundChannel;
		}

		/**
		 * Returns all info catched from the Vorbis header
		 *
		 * @return VorbisInfo
		 */
		public function get info():VorbisInfo
		{
			return _info;
		}

		/**
		 * @return true, if Vorbis will be looped.
		 */
		public function get loop():Boolean
		{
			return _loop;
		}

		/**
		 * Sets loop property
		 *
		 * @param value Set to true, if Vorbis should loop.
		 */
		public function set loop( value:Boolean ):void
		{
			_loop = value;
		}

		/**
		 * @param value position in seconds
		 */
		public function set position( value:Number ):void
		{
			alchemy.vorbisSeekTime( value );

			_decoded = 0;
			_samplePosition = alchemy.vorbisSamplePosition();
		}

		/**
		 * @return position in seconds
		 */
		public function get position():Number
		{
			return _samplePosition * SAMPLING_RATE_INV;
		}

		/**
		 * @return duration in seconds
		 */
		public function get duration():Number
		{
			return _sampleDuration * SAMPLING_RATE_INV;
		}

		/**
		 * @return true, if Vorbis is fully loaded.
		 */
		public function get loaded():Boolean
		{
			return _loaded;
		}

		/**
		 * @return Returns the currently available number of bytes in this object.
		 */
		public function get bytesLoaded():int
		{
			return _bytesLoaded;
		}

		/**
		 * @return Returns the total number of bytes in this object.
		 */
		public function get bytesTotal():int
		{
			return _bytesTotal;
		}

		/**
		 * will clear this file in memory
		 * but will work if you start with the init();
		 */
		public function dispose():void
		{
			_soundChannel.stop();

			try{ stream.close(); } catch( e: Error ){}
			file.clear();

			if( null != _info )
			{
				alchemy.vorbisFree();
			}

			unregisterStreamEvents();
			
			// smth goes wrong here and Player crashing without any error

			_initDecoder = false;
			_decoded = 0;
			_bytesTotal = 0;
			_bytesLoaded = 0;
			_samplePosition = 0.0;
			_sampleDuration = Number.MAX_VALUE;
			_lastFileUpdate = 0;
			_loaded = false;
			_info = null;
		}

		public override function toString():String
		{
			return '[VorbisSound]';
		}

		private function sampleData( event:SampleDataEvent ):void
		{
			const data:ByteArray = event.data;

			const ready: Boolean = _isPlaying && _initDecoder && ( _samplePosition + _bufferTime * 44.1 < _sampleDuration || _loaded );

			var read:int;
			var write:int = PLAYBACK_BUFFER_SIZE;

			if( ready )
			{
				while( write )
				{
					if( 0 == _decoded )
					{
						if( _samplePosition >= _sampleDuration )
						{
							if( _loop )
							{
								alchemy.vorbisSeekTime( 0.0 );

								_samplePosition = 0.0;
							}
							else
							{
								dispatchEvent( eventSoundComplete );

								break;
							}

							dispatchEvent( eventSoundComplete );
						}

						_decoded = alchemy.vorbisDecode();

						if( 0 == _decoded )
						{
							break;
						}

						if( _samplePosition + _decoded > _sampleDuration )
						{
							_decoded = _sampleDuration - _samplePosition;
						}

						_outIndex = alchemy.vorbisDecPtr;
					}

					read = _decoded <= write ? _decoded : write;

					if( read )
					{
						alchemyRAM.position = _outIndex;

						write -= read;
						_decoded -= read;
						_outIndex += read << 3;
						_samplePosition += read;

						while( read-- )
						{
							data.writeFloat( alchemyRAM.readFloat() );
							data.writeFloat( alchemyRAM.readFloat() );
						}
					}
				}
			}

			while( write-- )
			{
				data.writeFloat( 0.0 );
				data.writeFloat( 0.0 );
			}
		}

		private function checkStream():void
		{
			if( _bytesLoaded >= HEADER_BYTES )
			{
				if( !_initDecoder )
				{
					_info = alchemy.vorbisInit( file, _bytesTotal );

					_initDecoder = true;
					_lastFileUpdate = getTimer();

					dispatchEvent( eventInit );
				}
			}
		}

		private function streamProgress( event:ProgressEvent ):void
		{
			const bytesAvailable:int = stream.bytesAvailable;

			stream.readBytes( file, _bytesLoaded, bytesAvailable );

			_bytesLoaded += bytesAvailable;
			_bytesTotal = event.bytesTotal;

			checkStream();

			const time: uint = getTimer();

			if( _initDecoder && (time - _lastFileUpdate > 250) )
			{
				const value: Number = alchemy.vorbisSampleDuration() * SAMPLING_RATE;

				if( _sampleDuration < value )
				{
					_sampleDuration = value;
				}

				_lastFileUpdate = time;
			}

			dispatchEvent( event );
		}

		private function streamError( event:IOErrorEvent ):void
		{
			unregisterStreamEvents();

			dispatchEvent( event );
		}

		private function streamComplete( event:Event ):void
		{
			if( _bytesLoaded != _bytesTotal )
			{
				throw new Error(); // once throwed an error. why? bad internet connection?
			}

			checkStream();

			if( !_initDecoder )
			{
				throw new Error();
			}

			_sampleDuration = alchemy.vorbisSampleDuration() * SAMPLING_RATE;

			if( _bufferTime > _sampleDuration )
				_bufferTime = 0.0;

			_loaded = true;

			unregisterStreamEvents();

			dispatchEvent( event );
		}

		private function registerStreamEvents():void
		{
			stream.addEventListener( ProgressEvent.PROGRESS, streamProgress );
			stream.addEventListener( IOErrorEvent.IO_ERROR, streamError );
			stream.addEventListener( Event.COMPLETE, streamComplete );
		}

		private function unregisterStreamEvents():void
		{
			stream.removeEventListener( ProgressEvent.PROGRESS, streamProgress );
			stream.removeEventListener( IOErrorEvent.IO_ERROR, streamError );
			stream.removeEventListener( Event.COMPLETE, streamComplete );
		}
	}
}